Skip to content

feat(channel list): add persistent channel sort toggle#1505

Merged
tellaho merged 3 commits into
mainfrom
tho/channel-sort
Jul 6, 2026
Merged

feat(channel list): add persistent channel sort toggle#1505
tellaho merged 3 commits into
mainfrom
tho/channel-sort

Conversation

@tellaho

@tellaho tellaho commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Category: new-feature
User Impact: Users can choose, per sidebar group, whether channels are ordered alphabetically or by recent activity.
Problem: The left nav had one fixed channel ordering, which made it harder to scan either by name or by what changed most recently depending on the user's current workflow.
Solution: This adds independent persisted Recent / A–Z sort preferences for each sidebar grouping — Starred, every custom section, Channels, Forums, and DMs — each with its own header control. Every group defaults to A–Z to preserve today's behavior, and group boundaries stay untouched. Preferences are stored per pubkey + relay; sort modes for deleted custom sections are pruned on write so they don't accumulate in localStorage.

File changes

desktop/src/features/sidebar/lib/channelSortPreference.ts
Introduces the localStorage-backed per-group sort store (versioned groups map keyed by starred / channels / forums / dms / section:<id>), the shared channel sorting helper, and stripOrphanedSectionModes which prunes stale section:<id> entries for deleted custom sections.

desktop/src/features/sidebar/lib/channelSortPreference.test.mjs
Covers payload sanitization, per-group defaults and independence, orphaned-section pruning, recent ordering, missing/unparseable activity, and deterministic tie-breaks.

desktop/src/features/sidebar/lib/dmSidebarSort.ts
Makes DM sorting mode-aware without changing the resolved-label ordering users already see by default.

desktop/src/features/sidebar/lib/dmSidebarSort.test.mjs
Extends DM sorting tests so DMs keep their existing A–Z behavior while supporting recent activity ordering.

desktop/src/features/sidebar/lib/useChannelSortPreference.ts
Adds a hook for reading and updating each group's sort mode, syncing across windows via the storage event, and pruning deleted-section entries on write using the live section ids.

desktop/src/features/sidebar/ui/AppSidebar.tsx
Wires each grouping's own sort preference into its bucket (starred, per-section, unassigned channels, forums, DMs) and passes live section ids into the preference hook; section assignment and drag-and-drop paths are untouched.

desktop/src/features/sidebar/ui/ChannelSortDropdown.tsx
Adds the compact Recent / A–Z dropdown control rendered in each group's header action cluster using the existing dropdown radio pattern.

desktop/src/features/sidebar/ui/CustomChannelSection.tsx
Lets custom section headers host their own sort control without one-off layout behavior.

Reproduction Steps

  1. Open the desktop app and look at the left sidebar channel list.
  2. Hover a group header (Starred, a custom section, Channels, Forums, or DMs) and open its ↑↓ sort dropdown.
  3. Confirm A–Z is selected by default and channels remain grouped as Starred, custom sections, Channels, Forums, and DMs.
  4. Select Recent on one group and confirm only that group reorders by latest message activity — other groups keep their own mode and boundaries stay in place.
  5. Refresh or reopen another app window and confirm each group's sort mode persists for the same identity and relay.
  6. Delete a custom section that had a saved sort mode, change any group's sort, and confirm the deleted section's entry is removed from the stored preference payload.

Screenshots/Demos

Tight crop of the header — the ↑↓ is the ingress (each group header has its own):
image

Dropdown open (default A–Z):
image

A–Z Recent
image image

Notes

  • In Recent mode, ordering freshness is bounded by channelsQuery's ~60s foreground refetch (plus mutation/reconnect invalidations); incoming messages for already-known channels don't directly patch lastMessageAt, so the order can lag up to that refresh window.

Validation

  • biome check — clean on sidebar feature (repo-wide: 4 pre-existing warnings in pulse/hooks.ts)
  • pnpm typecheck — clean
  • pnpm test — 1605 pass / 0 fail (includes 4 new orphaned-section pruning tests)

@tellaho tellaho changed the title add persistent channel sort toggle feat(channel list): add persistent channel sort toggle Jul 3, 2026
@tellaho tellaho enabled auto-merge (squash) July 3, 2026 20:45
@tellaho tellaho marked this pull request as draft July 3, 2026 20:49
auto-merge was automatically disabled July 3, 2026 20:49

Pull request was converted to draft

@tellaho tellaho marked this pull request as ready for review July 4, 2026 03:17

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review (Brain, agent review on Wes's behalf) — one product question, otherwise sound

The question worth settling before merge: the PR body and the implementation disagree about scope. The body says "This adds one persisted sidebar sort preference… and applies it within each existing grouping," and the screenshots show a single dropdown on the Channels header. The code actually ships independent per-group preferences — a separate ↑↓ dropdown on Starred, every custom section, Channels, Forums, and DMs, each with its own saved mode (e2e test 03 explicitly asserts group independence). Per-group is defensible, but it's 5+ header controls instead of 1, and it's not what the description sells. Which one is the product intent? If "one preference," the store/UI should collapse to a single mode; if per-group, the body needs rewriting to match.

Technical review — no blockers:

  • Default A–Z genuinely preserves today's order: useChannelsQuery.sortChannels already sorts by name, and the new comparator only adds an id tie-break. DMs keep the resolved-label sort in alpha mode. ✅
  • Persistence is properly scoped pubkey+relay via the same normalizeRelayUrl pattern as channelSectionsStorage, with payload sanitization and cross-window storage sync. ✅
  • Comparators are pure, non-mutating, deterministic (recency → name → id), quiet channels sink alphabetically — all unit-pinned. ✅
  • CI fully green, including the new smoke spec.

Minor, non-blocking:

  1. Deleted custom sections leave orphaned section:<id> keys in the stored groups map forever — harmless but a slow localStorage leak. A one-line prune against live section ids on write would close it.
  2. In "Recent" mode, ordering freshness is bounded by channelsQuery's 60s refetch/invalidations, so the sidebar can lag reality by up to a minute — and when it does update, rows move under the cursor. Worth knowing, not worth blocking.

@tellaho

tellaho commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

🤖 Thanks for the review, Wes — I pushed updates addressing the feedback:

  • Clarified the product intent in the PR body: sorting is intentionally persisted independently per sidebar group (Starred, each custom section, Channels, Forums, DMs), not as one global preference.
  • Fixed cleanup for deleted custom sections: stale section:<id> sort-mode entries are pruned against the live section IDs while fixed groups are preserved. Added focused unit coverage for pruning, preserving fixed groups, same-reference no-op, and non-mutation.
  • Documented the Recent-mode freshness tradeoff: ordering is bounded by the existing channelsQuery refresh behavior, so tightening live recency updates would be a separate query/cache-layer change outside this PR.

CI was green on the updated head before the latest base conflict; I’m resolving that conflict now.

npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w and others added 3 commits July 6, 2026 16:17
Adds a user-toggleable sort preference for the left-nav channel lists.
One persisted preference (localStorage, scoped by pubkey + relay like
channel sections) applied inside every grouping - Starred, each custom
section, unassigned Channels, Forums, and DMs - without changing
grouping boundaries, section assignments, or drag-and-drop.

Recent sorts by lastMessageAt (newest first); channels with no parseable
activity sink to the bottom alphabetically. A-Z is the default so
existing ordering is preserved until the user opts in. The control is a
small sort dropdown in the Channels section header action cluster,
mirroring the inbox filter dropdown pattern.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Replaces the single sidebar-wide sort preference with an independent
per-group map persisted as {version, groups} under the same
pubkey+relay-scoped localStorage key. Group keys are starred, channels,
forums, dms, and section:<sectionId> (section IDs are stable UUIDs, so
prefs survive rename/reorder). Any unset group defaults to A-Z.

Every group header - Starred, each custom section, Channels, Forums,
and DMs - now gets its own sort dropdown at the front of its action
cluster. Adds a focused e2e spec covering default ordering, Recent
reorder with reload persistence, independent group preferences, and
the DM group trigger.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Deleted custom sections previously left their section:<id> sort modes in
the persisted store forever — a harmless but unbounded localStorage leak.
Writes now strip entries whose section id is no longer live, keeping the
fixed group keys (starred/channels/forums/dms) untouched.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
@tellaho tellaho force-pushed the tho/channel-sort branch from 04d0a72 to 2fe7f2c Compare July 6, 2026 23:18
@tellaho tellaho merged commit 36ac524 into main Jul 6, 2026
25 checks passed
@tellaho tellaho deleted the tho/channel-sort branch July 6, 2026 23:31
klopez4212 pushed a commit that referenced this pull request Jul 7, 2026
Re-applies the section-actions work from #1424 (silently clobbered by
#1428) and extends it: a single reusable SectionActionsMenu component now
renders the rightmost action for every sidebar section (Channels, Forums,
Direct messages, Starred, and custom sections).

- Fold the standalone Sort dropdown (#1505) into the ⋮ menu as a Sort
  radio group; delete ChannelSortDropdown.
- Move Browse channels / New channel / New forum / New message into the
  menu; drop the standalone header icon buttons.
- Remove the visible GripVertical grip from custom sections (drag handle
  props retained, so drag-to-reorder still works).
- deferMenuAction (setTimeout 0) avoids the modal/pointer-events freeze
  when a menu item opens a dialog.
- Keep the #1516 emoji/icon picker on custom sections.

Updates e2e helpers (openCreateChannelDialog / openNewDirectMessageDialog)
and specs (channel-sort, channels, identity-archive-hide) to drive the new
menu.

Signed-off-by: npub13fn4ahfnvaa2qwylvegdgeajqs0mph6v4qsw4jcqnw4mjh3hzh2quuucm5 <8a675edd33677aa0389f6650d467b2041fb0df4ca820eacb009babb95e3715d4@sprout-oss.stage.blox.sqprod.co>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants